Crate state

source · []
Expand description

Safe, Effortless state Management

This crate allows you to safely and effortlessly manage global and/or thread-local state. Three primitives are provided for state management:

  • Container: Type-based storage for many values.
  • Storage: Lazy storage for a single value.
  • LocalStorage: Lazy thread-local storage for a single value.

Usage

Include state in your Cargo.toml [dependencies]:

[dependencies]
state = "0.5"

Thread-local state management is not enabled by default. You can enable it via the tls feature:

[dependencies]
state = { version = "0.5", features = ["tls"] }

Use Cases

Memoizing Expensive Operations

The Storage type can be used to conveniently memoize expensive read-based operations without needing to mutably borrow. Consider a struct with a field value and method compute() that performs an expensive operation on value to produce a derived value. We can use Storage to memoize compute():

use state::Storage;

struct Value;
struct DerivedValue;

struct Foo {
    value: Value,
    cached: Storage<DerivedValue>
}

impl Foo {
    fn set_value(&mut self, v: Value) {
        self.value = v;
        self.cached = Storage::new();
    }

    fn compute(&self) -> &DerivedValue {
        self.cached.get_or_set(|| {
            let _value = &self.value;
            unimplemented!("expensive computation with `self.value`")
        })
    }
}

Read-Only Singleton

Suppose you have the following structure which is initialized in main after receiving input from the user:

struct Configuration {
    name: String,
    number: isize,
    verbose: bool
}

fn main() {
    let config = Configuration {
        /* fill in structure at run-time from user input */
    };
}

You’d like to access this structure later, at any point in the program, without any synchronization overhead. Prior to state, assuming you needed to setup the structure after program start, your options were:

  1. Use static mut and unsafe to set an Option<Configuration> to Some. Retrieve by checking for Some.
  2. Use lazy_static with a RwLock to set an RwLock<Option<Configuration>> to Some. Retrieve by locking and checking for Some, paying the cost of synchronization.

With state, you can use LocalStorage as follows:

static CONFIG: LocalStorage<Configuration> = LocalStorage::new();

fn main() {
    CONFIG.set(|| Configuration {
        /* fill in structure at run-time from user input */
    });

    /* at any point later in the program, in any thread */
    let config = CONFIG.get();
}

Note that you can also use Storage to the same effect.

Read/Write Singleton

Following from the previous example, let’s now say that we want to be able to modify our singleton Configuration structure as the program evolves. We have two options:

  1. If we want to maintain the same state in any thread, we can use a Storage structure and wrap our Configuration structure in a synchronization primitive.
  2. If we want to maintain different state in any thread, we can continue to use a LocalStorage structure and wrap our LocalStorage type in a Cell structure for internal mutability.

In this example, we’ll choose 1. The next example illustrates an instance of 2.

The following implements 1 by using a Storage structure and wrapping the Configuration type with a RwLock:

static CONFIG: Storage<RwLock<Configuration>> = Storage::new();

fn main() {
    let config = Configuration {
        /* fill in structure at run-time from user input */
    };

    // Make the config avaiable globally.
    CONFIG.set(RwLock::new(config));

    /* at any point later in the program, in any thread */
    let mut_config = CONFIG.get().write();
}

Mutable, thread-local data

Imagine you want to count the number of invocations to a function per thread. You’d like to store the count in a Cell<usize> and use count.set(count.get() + 1) to increment the count. Prior to state, your only option was to use the thread_local! macro. state provides a more flexible, and arguably simpler solution via LocalStorage. This scanario is implemented in the folloiwng:

static COUNT: LocalStorage<Cell<usize>> = LocalStorage::new();

fn function_to_measure() {
    let count = COUNT.get();
    count.set(count.get() + 1);
}

fn main() {
    // setup the initializer for thread-local state
    COUNT.set(|| Cell::new(0));

    // spin up many threads that call `function_to_measure`.
    let mut threads = vec![];
    for i in 0..10 {
        threads.push(thread::spawn(|| {
            // Thread IDs may be reusued, so we reset the state.
            COUNT.get().set(0);
            function_to_measure();
            COUNT.get().get()
        }));
    }

    // retrieve the total
    let total: usize = threads.into_iter()
        .map(|t| t.join().unwrap())
        .sum();

    assert_eq!(total, 10);
}

Correctness

state has been extensively vetted, manually and automatically, for soundness and correctness. All unsafe code, including in internal concurrency primitives, Container, and Storage are exhaustively verified for pairwise concurrency correctness and internal aliasing exclusion with loom. Multithreading invariants, aliasing invariants, and other soundness properties are verified with miri. Verification is run by the CI on every commit.

Performance

state is heavily tuned to perform optimally. get{_local} and set{_local} calls to a Container incur overhead due to type lookup. Storage, on the other hand, is optimal for global storage retrieval; it is slightly faster than accessing global state initialized through lazy_static!, more so across many threads. LocalStorage incurs slight overhead due to thread lookup. However, LocalStorage has no synchronization overhead, so retrieval from LocalStorage is faster than through Storage across many threads.

Bear in mind that state allows global initialization at any point in the program. Other solutions, such as lazy_static! and thread_local! allow initialization only a priori. In other words, state’s abilities are a superset of those provided by lazy_static! and thread_local! while being more performant.

When To Use

You should avoid using global state as much as possible. Instead, thread state manually throughout your program when feasible.

Macros

Type constructor for Container variants.

Structs

A container for global type-based state.

A single storage location for global access to thread-local values.

A single storage location for global access to a value.